﻿#region Using

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

#endregion

namespace Commons.Reflection.IL
{
    public static class IlReader
    {
        /// <summary>
        /// Retorna uma string contendo o código em instruções IL legiveis.
        /// </summary>
        /// <returns></returns>
        public static StringBuilder GetMethodBodyCode( MethodBase aMethodBase )
        {
            Contract.Requires( aMethodBase != null );

            MethodBody lMethodBody = aMethodBase.GetMethodBody();
            //Metodo não possui corpo
            if ( lMethodBody == null )
                return new StringBuilder();

            #region Recupera o corpo da função

            //Primeiro passo, limpa qualqueis dados que pudessem estar na lista.
            List<IlInstruction> lLstIlInstruction = new List<IlInstruction>();
            //Separa numa variavel local o array de bytes, para evitar ficar chamando a função direto.
            byte[] lIlBytes = lMethodBody.GetILAsByteArray();
            //Guarda o total do deslocamente pelo array de bytes.
            int lIlOffset = 0;
            //Agora, enquanto não chegamos ao final do array, faz-se a leitura de cada um dos pares de instruções.
            while ( lIlOffset < lIlBytes.Length )
            {
                //Declara a variavel que irá buscar o operador
                OpCode lOperator;
                //E a variavel que irá buscar o operando, que inicialmente está na primeira posição do array.
                //Aproveita para incrementar também a variavel que controla o deslocamento.
                ushort lOpCodeValue = lIlBytes[lIlOffset++];
                // Verifica se é um operador com multi argumentos...
                if ( IsMultiArgumentsOperator( lOpCodeValue ) )
                {
                    //Neste caso, busca o operador que está na posição seguinte, e aproveita para incrementar a variavel de deslocamente do array.
                    lOpCodeValue = lIlBytes[lIlOffset++];
                    //Recupera o operador de dentro do dicionário.
                    lOperator = ProcessedOpCodes.DctMultiByteOpCodes[lOpCodeValue];
                }
                else
                {
                    //Recupera o operador de dentro do dicionário.
                    lOperator = ProcessedOpCodes.DctSingleByteOpCodes[lOpCodeValue];
                }

                //Separa o objeto de módulo ao qual a função pertence.
                Module lModule = aMethodBase.Module;
                //Cria o objeto de instruções IL
                IlInstruction lIlInstruction = new IlInstruction
                {
                    Operator = lOperator,
                    Offset = lIlOffset - 1
                };
                //Variavel auxiliar para resgatar o operando.
                int lOperandValue;
                //Agora recupera o operando da operação atual.
                switch ( lOperator.OperandType )
                {
                    case OperandType.InlineBrTarget:
                        lOperandValue = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        lOperandValue += lIlOffset;
                        lIlInstruction.Operand = lOperandValue;
                        break;
                    case OperandType.InlineField:
                        lOperandValue = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        if ( aMethodBase is ConstructorInfo )
                        {
                            lIlInstruction.Operand = lModule.ResolveField( lOperandValue,
                                                                           aMethodBase.DeclaringType.GetGenericArguments(),
                                                                           null );
                        }
                        else
                        {
                            lIlInstruction.Operand = lModule.ResolveField( lOperandValue,
                                                                           aMethodBase.DeclaringType.GetGenericArguments(),
                                                                           aMethodBase.GetGenericArguments() );
                        }
                        break;
                    case OperandType.InlineMethod:
                        lOperandValue = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        try
                        {
                            if ( aMethodBase is ConstructorInfo )
                            {
                                lIlInstruction.Operand = lModule.ResolveMethod( lOperandValue,
                                                                                aMethodBase.DeclaringType.GetGenericArguments(),
                                                                                null );
                            }
                            else
                            {
                                lIlInstruction.Operand = lModule.ResolveMethod( lOperandValue,
                                                                                aMethodBase.DeclaringType.GetGenericArguments(),
                                                                                aMethodBase.GetGenericArguments() );
                            }
                        }
                        catch
                        {
                            if ( aMethodBase is ConstructorInfo )
                            {
                                lIlInstruction.Operand = lModule.ResolveMember( lOperandValue,
                                                                                aMethodBase.DeclaringType.GetGenericArguments(),
                                                                                null );
                            }
                            else
                            {
                                lIlInstruction.Operand = lModule.ResolveMember( lOperandValue,
                                                                                aMethodBase.DeclaringType.GetGenericArguments(),
                                                                                aMethodBase.GetGenericArguments() );
                            }
                        }
                        break;
                    case OperandType.InlineSig:
                        lOperandValue = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        lIlInstruction.Operand = lModule.ResolveSignature( lOperandValue );
                        break;
                    case OperandType.InlineTok:
                        lOperandValue = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        try
                        {
                            if ( aMethodBase is ConstructorInfo )
                            {
                                lIlInstruction.Operand = lModule.ResolveType( lOperandValue,
                                                                              aMethodBase.DeclaringType.GetGenericArguments(),
                                                                              null );
                            }
                            else
                            {
                                lIlInstruction.Operand = lModule.ResolveType( lOperandValue,
                                                                              aMethodBase.DeclaringType.GetGenericArguments(),
                                                                              aMethodBase.GetGenericArguments() );
                            }
                        }
                        catch {}
                        // SSS : see what to do here
                        break;
                    case OperandType.InlineType:
                        lOperandValue = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        if ( aMethodBase is MethodInfo )
                        {
                            lIlInstruction.Operand = lModule.ResolveType( lOperandValue,
                                                                          aMethodBase.DeclaringType.GetGenericArguments(),
                                                                          aMethodBase.GetGenericArguments() );
                        }
                        else if ( aMethodBase is ConstructorInfo )
                        {
                            lIlInstruction.Operand = lModule.ResolveType( lOperandValue,
                                                                          aMethodBase.DeclaringType.GetGenericArguments(),
                                                                          null );
                        }
                        else
                            lIlInstruction.Operand = lModule.ResolveType( lOperandValue );
                        break;
                    case OperandType.InlineI:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        break;
                    }
                    case OperandType.InlineI8:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadInt64( lIlBytes, ref lIlOffset );
                        break;
                    }
                    case OperandType.InlineNone:
                    {
                        lIlInstruction.Operand = null;
                        break;
                    }
                    case OperandType.InlineR:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadDouble( lIlBytes, ref lIlOffset );
                        break;
                    }
                    case OperandType.InlineString:
                    {
                        lOperandValue = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        lIlInstruction.Operand = lModule.ResolveString( lOperandValue );
                        break;
                    }
                    case OperandType.InlineSwitch:
                    {
                        int count = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        var casesAddresses = new int[count];
                        for ( int i = 0; i < count; i++ )
                        {
                            casesAddresses[i] = IlOperandReader.ReadInt32( lIlBytes, ref lIlOffset );
                        }
                        var cases = new int[count];
                        for ( int i = 0; i < count; i++ )
                        {
                            cases[i] = lIlOffset + casesAddresses[i];
                        }
                        break;
                    }
                    case OperandType.InlineVar:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadUInt16( lIlBytes, ref lIlOffset );
                        break;
                    }
                    case OperandType.ShortInlineBrTarget:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadSByte( lIlBytes, ref lIlOffset ) + lIlOffset;
                        break;
                    }
                    case OperandType.ShortInlineI:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadSByte( lIlBytes, ref lIlOffset );
                        break;
                    }
                    case OperandType.ShortInlineR:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadSingle( lIlBytes, ref lIlOffset );
                        break;
                    }
                    case OperandType.ShortInlineVar:
                    {
                        lIlInstruction.Operand = IlOperandReader.ReadByte( lIlBytes, ref lIlOffset );
                        break;
                    }
                    default:
                    {
                        throw new Exception( "Unknown operand type." );
                    }
                }
                //Finalmente, depois do switch, adiciona a instrução a lista de instruções.
                lLstIlInstruction.Add( lIlInstruction );
            }

            #endregion

            //Não foi identificada nenhuma instrução.
            if ( lLstIlInstruction.Count < 1 )
                return new StringBuilder();

            StringBuilder lMethodBodyString = new StringBuilder();

            lLstIlInstruction.ForEach( aIlInstruction => lMethodBodyString.AppendLine( aIlInstruction.ToString() ) );

            return lMethodBodyString;
        }

        /// <summary>
        ///   Informa se o operando atual está indicando que o método em questão possui argumentos para o operador IL.
        /// </summary>
        /// <param name = "aOperand"></param>
        /// <returns></returns>
        private static bool IsMultiArgumentsOperator( ushort aOperand )
        {
            return aOperand == 0xfe;
        }

        #region Nested type: IlOperandReader

        /// <summary>
        ///   Métodos de extensão para a leitura dos dados contidos em uma IL.
        /// </summary>
        internal static class IlOperandReader
        {
            public static int ReadInt16( byte[] aIlBytes, ref int position )
            {
                return ( ( aIlBytes[position++] | ( aIlBytes[position++] << 8 ) ) );
            }

            public static ushort ReadUInt16( byte[] aIlBytes, ref int position )
            {
                return (ushort) ( ( aIlBytes[position++] | ( aIlBytes[position++] << 8 ) ) );
            }

            public static int ReadInt32( byte[] aIlBytes, ref int position )
            {
                return ( ( ( aIlBytes[position++] | ( aIlBytes[position++] << 8 ) ) | ( aIlBytes[position++] << 0x10 ) ) | ( aIlBytes[position++] << 0x18 ) );
            }

            public static ulong ReadInt64( byte[] aIlBytes, ref int position )
            {
                return
                    (ulong)
                    ( ( ( aIlBytes[position++] | ( aIlBytes[position++] << 8 ) ) | ( aIlBytes[position++] << 0x10 ) ) | ( aIlBytes[position++] << 0x18 ) |
                      ( aIlBytes[position++] << 0x20 ) |
                      ( aIlBytes[position++] << 0x28 ) | ( aIlBytes[position++] << 0x30 ) | ( aIlBytes[position++] << 0x38 ) );
            }

            public static double ReadDouble( byte[] aIlBytes, ref int position )
            {
                return ( ( ( aIlBytes[position++] | ( aIlBytes[position++] << 8 ) ) | ( aIlBytes[position++] << 0x10 ) ) | ( aIlBytes[position++] << 0x18 ) |
                         ( aIlBytes[position++] << 0x20 ) |
                         ( aIlBytes[position++] << 0x28 ) | ( aIlBytes[position++] << 0x30 ) | ( aIlBytes[position++] << 0x38 ) );
            }

            public static sbyte ReadSByte( byte[] aIlBytes, ref int position )
            {
                return (sbyte) aIlBytes[position++];
            }

            public static byte ReadByte( byte[] aIlBytes, ref int position )
            {
                return aIlBytes[position++];
            }

            public static Single ReadSingle( byte[] aIlBytes, ref int position )
            {
                return ( ( ( aIlBytes[position++] | ( aIlBytes[position++] << 8 ) ) | ( aIlBytes[position++] << 0x10 ) ) | ( aIlBytes[position++] << 0x18 ) );
            }
        }

        #endregion

        #region Nested type: ProcessedOpCodes

        internal static class ProcessedOpCodes
        {
            public static Dictionary<ushort, OpCode> DctMultiByteOpCodes;

            public static Dictionary<ushort, OpCode> DctSingleByteOpCodes;

            static ProcessedOpCodes()
            {
                //Reserva 256 espaços. => ( 0x100 hex == 256 dec )
                DctSingleByteOpCodes = new Dictionary<ushort, OpCode>( 0x100 );
                DctMultiByteOpCodes = new Dictionary<ushort, OpCode>( 0x100 );
                //Recupera os campos cujo tipo dele seja OpCode.
                var lFields =
                    from
                        sFieldInfo in typeof(OpCodes).GetFields()
                    where
                        sFieldInfo.FieldType == typeof(OpCode)
                    select
                        sFieldInfo;
                //Itera pelos campos, para distiguir qual deles cai em cada Array
                foreach ( FieldInfo lFieldInfo in lFields )
                {
                    //Recupera o campo de tipo OpCode da classe OpCodes.
                    OpCode lOpCode = (OpCode) lFieldInfo.GetValue( null );
                    //Separa o código do OpCode.
                    ushort lOpCodeValue = (ushort) lOpCode.Value;
                    //Verifica se o código do OpCode é menor que 256. => ( 0x100 hex == 256 dec )
                    if ( lOpCodeValue < 0x100 )
                    {
                        //Se for, é um operador de argumento único.
                        DctSingleByteOpCodes.Add( lOpCodeValue, lOpCode );
                    }
                    else
                    {
                        //Senão, é um operador de mais de um argumento. Mas confere antes pra ver se ele não passa de um código limite.
                        if ( ( lOpCodeValue & 0xff00 ) != 0xfe00 )
                            throw new Exception( "Invalid Operator!" );
                        DctMultiByteOpCodes.Add( (ushort) ( lOpCodeValue & 0xff ), lOpCode );
                    }
                }
            }
        }

        #endregion
    }
}